package org.dfl.core.model.transitions;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.dfl.core.data.types.Type;
import org.dfl.core.data.types.TypeContainer;
import org.dfl.core.data.types.TypeManager;
import org.dfl.core.model.TInputEdge;
import org.dfl.messages.Messages;

/**
 * transition that has many inputs
 * @author Piotr Wlodarczyk
 *
 */
public abstract class MultipleInputsTransition extends Transition {
	// definicje typow wejsc do tranzycji
	protected Map<String, TypeContainer> inputsDefinition;
	
	protected Set<String> optionalInputs = new HashSet<String>();
	
	// podlaczone wejscia do tranzycji
	private Map<String, TInputEdge> inputs;

	protected int maxInputs;	// -1 = no limit
	protected int minInputs;	// -1 = no limit

	public MultipleInputsTransition() {
		init();
	}

	// --------------------------------------------------- WEJSCIA
	// ------------------------------------------------- //
	@Override
	protected void init() {
		super.init();
		maxInputs = getDefaultMaxInputs();
		minInputs = getDefaultMinInputs();

		inputsDefinition = new HashMap<String, TypeContainer>();
		inputs = new HashMap<String, TInputEdge>();
	}

	protected abstract int getDefaultMinInputs();

	protected abstract int getDefaultMaxInputs();

	public int inputsCount() {
		return inputs.size();
	}

	public Set<TInputEdge> getInputEdges() {
		return new HashSet<TInputEdge>(inputs.values());
	}

	/**
	 * zwraca definicje inputow
	 * 
	 * @return
	 */
	public Map<String, TypeContainer> getInputsDefinition() {
		return inputsDefinition;
	}

	/**
	 * sprawdza, czy jest wolne jakies wejscie o danym typie
	 * 
	 * @param type
	 * @return
	 */
	public boolean hasFreeInput(Type type) {
		Collection<String> inputs = getFreeInputs(type);
		// null means that all inputs are available
		return (inputs == null || !inputs.isEmpty());
	}
	
	public boolean isInputNameNeeded() {
		return true;
	}

	/**
	 * pobiera kolekcje wolnych wejsc o danym typie
	 * 
	 * @param type
	 * @return collection of inputs names or null if any name is available
	 */
	public List<String> getFreeInputs(Type type) {
		if (maxInputs < 0 || maxInputs > inputs.size()) {
			return null;
		} else {
			return new ArrayList<String>();
		}
	}

	/**
	 * sprawdza, czy jest wolne wejscie o kluczu key
	 * 
	 * @param key
	 * @return
	 */
	protected boolean hasFreeInput(String key) {
		return ((maxInputs < 0 || maxInputs > inputs.size()) && !inputs.containsKey(key));
	}

	/**
	 * dodane nowe wejscie do tranzycji - musi byc wolne!
	 * 
	 * @param key
	 * @param e
	 * @throws Exception
	 */
	public void addInput(TInputEdge e) throws Exception {
		String key = e.getLabel();
		// first check availability of inputs
		if (!hasFreeInput(key)) {
			throw new Exception(getInputsDefinition().containsKey(key) ? Messages.MultipleInputsTransition_errorInputNameNotAllowed : Messages.MultipleInputsTransition_errorInputNotAllowed);
		}

		autoAddInputDefiniton(key);

		// then connect the types
		if (!e.getTypeContainer().connectWith(inputsDefinition.get(key))) {
			removeInput(key);
			throw new Exception(Messages.Transition_errorInputType);
		}

		// then add the input
		inputs.put(key, e);
	}

	/**
	 * dodaje nowa definicje inputa
	 * 
	 * @param key
	 * @param type
	 */
	public void addInputDefinition(String key, Type type) throws Exception {
		inputsDefinition.put(key, new TypeContainer(type));
	}

	protected void autoAddInputDefiniton(String key) throws Exception {
		if(isAutoAddRemoveInputDefinitions()){
			addInputDefinition(key, TypeManager.getAnyType());
		}
	}
	
	protected boolean isAutoAddRemoveInputDefinitions(){
		return true;
	}

	/**
	 * usun dane wejscie do tranzycji
	 * 
	 * @param key
	 */
	public void removeInput(String key) {
		inputs.remove(key);
		autoRemoveInputDefiniton(key);
	}

	public void removeInputDefinition(String key) {
		inputsDefinition.remove(key);
		if (inputs.containsKey(key)) {
			inputs.get(key).disconnect();
			inputs.remove(key);
		}
	}
	

	protected void autoRemoveInputDefiniton(String key){
		if(isAutoAddRemoveInputDefinitions()){
			removeInputDefinition(key);	
		}
	}
	
	@Override
	public void disconnectAllInputs() {
		Collection<TInputEdge> inputsToDisconnect = getInputEdges();
		for(TInputEdge input : inputsToDisconnect){
			input.disconnect();
		}
	}

	/**
	 * spdawdzenie, czy wszystkie inputy sa juz zdefiniowane=podpiete
	 * 
	 * @return
	 */
	public boolean areAllInputsDefined() {
		Set<String> neededKeys = new HashSet<String>(inputsDefinition.keySet());
		neededKeys.removeAll(inputs.keySet());
		neededKeys.removeAll(optionalInputs);
		return (inputs.size() >= minInputs && neededKeys.isEmpty());
	}

	
	public boolean isOptionalInput(String inputName) {
		return optionalInputs.contains(inputName);
	}

	protected void addOptionalInput(String optionalInput) {
		this.optionalInputs.add(optionalInput);
	}
	
	protected void removeOptionalInput(String optionalInput) {
		this.optionalInputs.remove(optionalInput);
	}

	// --------------------------------------------------- OPISY
	// ------------------------------------------------- //
	/**
	 * pobiera opis wejsc
	 * 
	 * @return
	 */
	protected String getInputDescription() {
		StringBuffer res = new StringBuffer();
		Collection<TInputEdge> edges = getInputEdges();
		for (TInputEdge e : edges) {
			String key = e.getLabel();
			String value = e.getTypeContainer().getType().toString();
			res.append("	" + key + "=" + value + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		}
		return res.toString();
	}
}
